Skip to content

Validate hinted methods#320

Open
shihab-dls wants to merge 6 commits intomainfrom
validate_methods
Open

Validate hinted methods#320
shihab-dls wants to merge 6 commits intomainfrom
validate_methods

Conversation

@shihab-dls
Copy link
Contributor

@shihab-dls shihab-dls commented Feb 13, 2026

Closes #300

This PR validated hinted scan and command methods, and splits validation logic into helper functions.

Summary by CodeRabbit

  • New Features

    • Support for validating and binding hinted methods (commands/scans) in controllers.
    • Expanded name-clash detection covering attributes, sub-controllers, commands, and scans.
  • Improvements

    • Centralized validation for consistent error handling and clearer logging/messages during initialization.
  • Tests

    • Updated and expanded tests to cover hinted-method validation, command/scan conflicts, and refined conflict messaging.

@coderabbitai
Copy link

coderabbitai bot commented Feb 13, 2026

📝 Walkthrough

Walkthrough

Adds tracking and validation for type-hinted Method objects in BaseController, centralizes name-clash detection across attributes, methods, and sub-controllers, updates add/validation flows to enforce hinted constraints, and re-exports Method from the methods package; tests updated to cover new validations.

Changes

Cohort / File(s) Summary
Controller core
src/fastcs/controllers/base_controller.py
Track hinted methods via __hinted_methods; recognize Method hints during type-hint discovery; add generic hinted-member validators (_validate_hinted_member, _validate_hinted_method, _validate_hinted_attribute, _validate_hinted_controller); add centralized _check_for_name_clash; enforce hint validation on add_attribute, add_sub_controller, add_command, and add_scan; add logging for validation steps.
Methods package export
src/fastcs/methods/__init__.py
Re-export Method from .method to expose it for type-hinting and public API.
Tests
tests/test_controllers.py
Adjust error message expectations; consolidate conflict tests into parameterized cases; add async noop and tests covering hinted-method introspection failures and command/scan validation paths; include Commands/Scans in conflict scenarios.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~30 minutes

Poem

🐰 I hop through hints with careful paws,

I check each name with thoughtful laws,
Methods bound where types were sought,
No clashes left, just tidy thought,
A rabbit's nod to tests well taught.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 32.14% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Validate hinted methods' directly summarizes the main change: adding validation for hinted methods across commands and scans.
Linked Issues check ✅ Passed The PR implements all requirements from issue #300: name clash detection helper, validation that hinted methods exist, and ensuring method assignment fails on name conflicts.
Out of Scope Changes check ✅ Passed All changes are directly related to issue #300 objectives: validation logic refactoring, name clash detection, hinted method validation, and updated tests.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch validate_methods

Tip

Issue Planner is now in beta. Read the docs and try it out! Share your feedback on Discord.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@codecov
Copy link

codecov bot commented Feb 13, 2026

Codecov Report

❌ Patch coverage is 96.42857% with 2 lines in your changes missing coverage. Please review.
✅ Project coverage is 90.89%. Comparing base (5d6b41c) to head (df6b7d4).
⚠️ Report is 1 commits behind head on main.

Files with missing lines Patch % Lines
src/fastcs/controllers/base_controller.py 96.36% 2 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #320      +/-   ##
==========================================
+ Coverage   90.84%   90.89%   +0.05%     
==========================================
  Files          70       70              
  Lines        2555     2592      +37     
==========================================
+ Hits         2321     2356      +35     
- Misses        234      236       +2     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@shihab-dls shihab-dls marked this pull request as ready for review February 13, 2026 13:14
@shihab-dls shihab-dls requested a review from GDYendell February 13, 2026 13:19
Copy link
Contributor

@GDYendell GDYendell left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor comments and a question

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we want the from when the base exception has no message? What does this look like? Maybe there should be a base message.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point; the exceptions without from exc just don't have:

Traceback (most recent call last):
  File "/workspaces/FastCS/src/fastcs/controllers/base_controller.py", line 212, in _validate_hinted_attribute
    attr = self._validate_hinted_member(name, Attribute)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/workspaces/FastCS/src/fastcs/controllers/base_controller.py", line 192, in _validate_hinted_member
    raise RuntimeError()
RuntimeError

The above exception was the direct cause of the following exception:

which is not must more informative than the rest of the exception:

Traceback (most recent call last):
  File "/cache/venv-for/scratch/rye74444/FastCS/lib/python3.11/site-packages/_pytest/runner.py", line 353, in from_call
    result: TResult | None = func()
                             ^^^^^^
  File "/cache/venv-for/scratch/rye74444/FastCS/lib/python3.11/site-packages/_pytest/runner.py", line 245, in <lambda>
    lambda: runtest_hook(item=item, **kwds),
            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/cache/venv-for/scratch/rye74444/FastCS/lib/python3.11/site-packages/pluggy/_hooks.py", line 512, in __call__
    return self._hookexec(self.name, self._hookimpls.copy(), kwargs, firstresult)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/cache/venv-for/scratch/rye74444/FastCS/lib/python3.11/site-packages/pluggy/_manager.py", line 120, in _hookexec
    return self._inner_hookexec(hook_name, methods, kwargs, firstresult)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/cache/venv-for/scratch/rye74444/FastCS/lib/python3.11/site-packages/pluggy/_callers.py", line 167, in _multicall
    raise exception
  File "/cache/venv-for/scratch/rye74444/FastCS/lib/python3.11/site-packages/pluggy/_callers.py", line 139, in _multicall
    teardown.throw(exception)
  File "/cache/venv-for/scratch/rye74444/FastCS/lib/python3.11/site-packages/_pytest/logging.py", line 850, in pytest_runtest_call
    yield
  File "/cache/venv-for/scratch/rye74444/FastCS/lib/python3.11/site-packages/pluggy/_callers.py", line 139, in _multicall
    teardown.throw(exception)
  File "/cache/venv-for/scratch/rye74444/FastCS/lib/python3.11/site-packages/_pytest/capture.py", line 900, in pytest_runtest_call
    return (yield)
            ^^^^^
  File "/cache/venv-for/scratch/rye74444/FastCS/lib/python3.11/site-packages/pluggy/_callers.py", line 139, in _multicall
    teardown.throw(exception)
  File "/cache/venv-for/scratch/rye74444/FastCS/lib/python3.11/site-packages/pluggy/_callers.py", line 53, in run_old_style_hookwrapper
    return result.get_result()
           ^^^^^^^^^^^^^^^^^^^
  File "/cache/venv-for/scratch/rye74444/FastCS/lib/python3.11/site-packages/pluggy/_result.py", line 103, in get_result
    raise exc.with_traceback(tb)
  File "/cache/venv-for/scratch/rye74444/FastCS/lib/python3.11/site-packages/pluggy/_callers.py", line 38, in run_old_style_hookwrapper
    res = yield
          ^^^^^
  File "/cache/venv-for/scratch/rye74444/FastCS/lib/python3.11/site-packages/pluggy/_callers.py", line 139, in _multicall
    teardown.throw(exception)
  File "/cache/venv-for/scratch/rye74444/FastCS/lib/python3.11/site-packages/_pytest/skipping.py", line 268, in pytest_runtest_call
    return (yield)
            ^^^^^
  File "/cache/venv-for/scratch/rye74444/FastCS/lib/python3.11/site-packages/pluggy/_callers.py", line 121, in _multicall
    res = hook_impl.function(*args)
          ^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/cache/venv-for/scratch/rye74444/FastCS/lib/python3.11/site-packages/_pytest/runner.py", line 179, in pytest_runtest_call
    item.runtest()
  File "/cache/venv-for/scratch/rye74444/FastCS/lib/python3.11/site-packages/_pytest/python.py", line 1720, in runtest
    self.ihook.pytest_pyfunc_call(pyfuncitem=self)
  File "/cache/venv-for/scratch/rye74444/FastCS/lib/python3.11/site-packages/pluggy/_hooks.py", line 512, in __call__
    return self._hookexec(self.name, self._hookimpls.copy(), kwargs, firstresult)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/cache/venv-for/scratch/rye74444/FastCS/lib/python3.11/site-packages/pluggy/_manager.py", line 120, in _hookexec
    return self._inner_hookexec(hook_name, methods, kwargs, firstresult)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/cache/venv-for/scratch/rye74444/FastCS/lib/python3.11/site-packages/pluggy/_callers.py", line 167, in _multicall
    raise exception
  File "/cache/venv-for/scratch/rye74444/FastCS/lib/python3.11/site-packages/pluggy/_callers.py", line 139, in _multicall
    teardown.throw(exception)
  File "/cache/venv-for/scratch/rye74444/FastCS/lib/python3.11/site-packages/pluggy/_callers.py", line 53, in run_old_style_hookwrapper
    return result.get_result()
           ^^^^^^^^^^^^^^^^^^^
  File "/cache/venv-for/scratch/rye74444/FastCS/lib/python3.11/site-packages/pluggy/_result.py", line 103, in get_result
    raise exc.with_traceback(tb)
  File "/cache/venv-for/scratch/rye74444/FastCS/lib/python3.11/site-packages/pluggy/_callers.py", line 38, in run_old_style_hookwrapper
    res = yield
          ^^^^^
  File "/cache/venv-for/scratch/rye74444/FastCS/lib/python3.11/site-packages/pluggy/_callers.py", line 121, in _multicall
    res = hook_impl.function(*args)
          ^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/cache/venv-for/scratch/rye74444/FastCS/lib/python3.11/site-packages/_pytest/python.py", line 166, in pytest_pyfunc_call
    result = testfunction(**testargs)
             ^^^^^^^^^^^^^^^^^^^^^^^^
  File "/workspaces/FastCS/tests/test_controllers.py", line 172, in test_attribute_hint_validation
    controller._validate_type_hints()
  File "/workspaces/FastCS/src/fastcs/controllers/base_controller.py", line 177, in _validate_type_hints
    self._validate_hinted_attribute(name)
  File "/workspaces/FastCS/src/fastcs/controllers/base_controller.py", line 214, in _validate_hinted_attribute
    raise RuntimeError(
RuntimeError: Controller `HintedController` failed to introspect hinted attribute `read_write_int` during initialisation

so I think we just:

raise RuntimeError(
                f"Controller `{self.__class__.__name__}` failed to introspect "
                f"hinted attribute `{name}` during initialisation"
            ) from None

since there isn't more context the base exception needs to provide

@shihab-dls shihab-dls requested a review from GDYendell February 18, 2026 15:18
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (3)
src/fastcs/controllers/base_controller.py (3)

188-193: member is None check is redundant

isinstance(None, expected_type) already evaluates to False for all relevant types (Attribute, Method, BaseController), making the explicit None guard superfluous.

♻️ Proposed simplification
 def _validate_hinted_member(self, name: str, expected_type: type):
     """Validate that a hinted member exists on the controller"""
     member = getattr(self, name, None)
-    if member is None or not isinstance(member, expected_type):
+    if not isinstance(member, expected_type):
         raise RuntimeError()
     return member
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/fastcs/controllers/base_controller.py` around lines 188 - 193, The check
"member is None" in _validate_hinted_member is redundant because
isinstance(None, expected_type) is always False; update the function (named
_validate_hinted_member) to remove the explicit None guard and simply use "if
not isinstance(member, expected_type): raise RuntimeError(...)" so it still
raises for missing/wrong types (e.g., Attribute, Method, BaseController) and
optionally include a helpful message in the RuntimeError for debugging.

347-380: Type-mismatch detail from _validated_added_method is swallowed by the error wrapper

Both _check_for_name_clash and _validated_added_method sit inside the same try block, so the rich "Expected Command got Scan" message from _validated_added_method is replaced by the generic "Cannot add command/scan method X." — it's only recoverable via __cause__.

add_attribute avoids this by placing the hint-type check outside the try block, preserving its message as the primary exception. Aligning add_command/add_scan with that pattern would make type mismatch errors immediately actionable:

♻️ Proposed fix for `add_command` and `add_scan`
 def add_command(self, name: str, command: Command):
     try:
         self._check_for_name_clash(name)
     except ValueError as exc:
         raise ValueError(f"Cannot add command method {command}.") from exc
+    self._validated_added_method(name, command)

-    try:
-        self._check_for_name_clash(name)
-        self._validated_added_method(name, command)
-    except (ValueError, RuntimeError) as exc:
-        raise exc.__class__(f"Cannot add command method {command}.") from exc

     self.__command_methods[name] = command
     super().__setattr__(name, command)

 def add_scan(self, name: str, scan: Scan):
     try:
         self._check_for_name_clash(name)
     except ValueError as exc:
         raise ValueError(f"Cannot add scan method {scan}.") from exc
+    self._validated_added_method(name, scan)

-    try:
-        self._check_for_name_clash(name)
-        self._validated_added_method(name, scan)
-    except (ValueError, RuntimeError) as exc:
-        raise exc.__class__(f"Cannot add scan method {scan}.") from exc

     self.__scan_methods[name] = scan
     super().__setattr__(name, scan)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/fastcs/controllers/base_controller.py` around lines 347 - 380, The try
block in add_command and add_scan currently wraps both _check_for_name_clash and
_validated_added_method so a RuntimeError from _validated_added_method is
masked; change the flow to call _check_for_name_clash inside a try/except that
only catches and re-raises the name-clash ValueError, then call
_validated_added_method(name, ...) outside that try so its RuntimeError (the
detailed "Expected X got Y" message) bubbles up as the primary exception; apply
the same pattern to both add_command and add_scan and keep using
__command_methods/__scan_methods and super().__setattr__ as before.

200-203: Ruff TRY003: Long exception messages defined outside exception classes

All the new/changed raise RuntimeError(…) and raise ValueError(…) sites across these lines are flagged by Ruff TRY003. The conventional fix is to define dedicated exception subclasses (e.g., ControllerInitialisationError) and put the message there, though given the scope this is a non-blocking style preference.

Also applies to: 214-217, 228-231, 285-287, 293-293, 324-324, 351-356, 363-363, 377-377

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/fastcs/controllers/base_controller.py` around lines 200 - 203, Create
dedicated exception subclasses (e.g., ControllerInitialisationError and
ControllerIntrospectionError) in this module and move the long messages into
those classes (as default message in class body or __init__) instead of
constructing long strings at each raise site; then replace occurrences of raise
RuntimeError(...) and raise ValueError(...) (including the one using
self.__class__.__name__ and the hinted method `name`, and the other flagged
sites) with raising the appropriate new exception class (preserve exception
chaining like `from None` where used).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/fastcs/controllers/base_controller.py`:
- Line 186: The suppression comment `# noqa: SLF001` on the call to
subcontroller._validate_type_hints() is unused; remove the `# noqa: SLF001`
suffix from that line so the call is simply subcontroller._validate_type_hints()
and run linting to confirm no further suppressions for SLF001 are present.

---

Nitpick comments:
In `@src/fastcs/controllers/base_controller.py`:
- Around line 188-193: The check "member is None" in _validate_hinted_member is
redundant because isinstance(None, expected_type) is always False; update the
function (named _validate_hinted_member) to remove the explicit None guard and
simply use "if not isinstance(member, expected_type): raise RuntimeError(...)"
so it still raises for missing/wrong types (e.g., Attribute, Method,
BaseController) and optionally include a helpful message in the RuntimeError for
debugging.
- Around line 347-380: The try block in add_command and add_scan currently wraps
both _check_for_name_clash and _validated_added_method so a RuntimeError from
_validated_added_method is masked; change the flow to call _check_for_name_clash
inside a try/except that only catches and re-raises the name-clash ValueError,
then call _validated_added_method(name, ...) outside that try so its
RuntimeError (the detailed "Expected X got Y" message) bubbles up as the primary
exception; apply the same pattern to both add_command and add_scan and keep
using __command_methods/__scan_methods and super().__setattr__ as before.
- Around line 200-203: Create dedicated exception subclasses (e.g.,
ControllerInitialisationError and ControllerIntrospectionError) in this module
and move the long messages into those classes (as default message in class body
or __init__) instead of constructing long strings at each raise site; then
replace occurrences of raise RuntimeError(...) and raise ValueError(...)
(including the one using self.__class__.__name__ and the hinted method `name`,
and the other flagged sites) with raising the appropriate new exception class
(preserve exception chaining like `from None` where used).

for subcontroller in self.sub_controllers.values():
subcontroller._validate_type_hints() # noqa: SLF001

def _validate_hinted_member(self, name: str, expected_type: type):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is missing a return type. I think it should be

Suggested change
def _validate_hinted_member(self, name: str, expected_type: type):
def _validate_hinted_member(self, name: str, expected_type: T) -> T:

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add validation of hinted methods on set and post initialise

2 participants

Comments